/*
 * Copyright (C) 2009, 2010, B3log Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.b3log.nide.util.parser.java;

import java.lang.reflect.Method;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.b3log.nide.util.parser.Style;
import org.b3log.nide.util.parser.marker.HighlightingMarker;
import org.b3log.nide.util.parser.marker.IdentifierMarker;
import org.b3log.nide.util.parser.java.source.JavaAST;

/**
 * Nide Java parser.
 *
 * @author <a href="mailto:DL88250@gmail.com">Liang Ding</a>
 * @version 1.0.1.0, Nov 29, 2010
 */
public final class NideJavaParser {

    /**
     * Logger.
     */
    private static final Logger LOGGER =
            Logger.getLogger(NideJavaParser.class.getName());
    /**
     * Java source code.
     */
    private String javaSourceCode;
    /**
     * Java abstract syntax tree.
     */
    private JavaAST javaAST;

    /**
     * Public constructor with the specified Java source code.
     *
     * @param javaSourceCode the specified Java source code
     */
    public NideJavaParser(final String javaSourceCode) {
        this.javaSourceCode = javaSourceCode;

        javaAST = new JavaAST(javaSourceCode);
    }

    /**
     * Gets import declaration names.
     *
     * @return import declaration names
     */
    public List<String> getImportNames() {
        return javaAST.getImports();
    }

    /**
     * Gets style at the specified line number and end column number.
     *
     * @param lineNum the specified line number(beginning at 1)
     * @param columnNum the specified column number(beginning at 1)
     * @return a style
     */
    public Style getStyle(final int lineNum, final int columnNum) {
        final HighlightingMarker highlightingMarker =
                javaAST.getHighlightingMarker(lineNum, columnNum);
        if (null == highlightingMarker) {
            return new Style();
        }

        return highlightingMarker.getStyle();
    }

    /**
     * Gets the type(primitive type or reference type) of a local variable at
     * the specified {@code lineNum}, and {@code endColumnNum} in method.
     *
     * @param lineNum the specified line number(beginning at 1)
     * @param endColumnNum the specified end column number(beginning at 1)
     * @return if found a local variable at the specified location, returns its
     *          type, {@code null} otherwise
     */
    public Class<?> getLocalVariableType(final int lineNum,
                                         final int endColumnNum) {
        LOGGER.log(Level.FINER,
                   "Getting local variable type at [lineNum={0}, columnNum={1}]",
                   new Object[]{lineNum, endColumnNum});
        final IdentifierMarker identifierMarker =
                javaAST.getIdentifierMarker(lineNum, endColumnNum);
        Class<?> ret = null;

        if (identifierMarker != null) {
            final String typeName = identifierMarker.getIdentifierType();

            LOGGER.log(Level.FINER, "Try loading class[canonicalName={0}]",
                       typeName);
            ret = loadClassByCanonicalName(typeName);

            if (null == ret) {
                LOGGER.log(Level.FINER,
                           "Try loading class[simpleName={0}] from package[name=java.lang]",
                           typeName);
                ret = loadClassUnderJavaLangPackage(typeName);
            }

            if (null == ret) {
                LOGGER.log(Level.FINER,
                           "Try loading class[simleName={0}] from imports",
                           typeName);
                ret = loadClassFromImports(typeName);
            }
        }

        return ret;
    }

    /**
     * Gets all public methods of the type of variable or method return before
     * the specified period location.
     * 
     * <ol>
     *   <li>Gets local variable type before period location, if not found class
     *   at the specified period location, try to</li>
     *   <li>get a method return type before period location, if not found class
     *   at the specified period location, returns {@code null}</li>
     * </ol>
     *
     * @param lineNum line number of period location
     * @param columnNum column number of period location
     * @return if found the type of variable or method return before the
     *         specified period location, returns the public methods of the
     *         type, {@code null} otherwise
     */
    public Method[] getMethods(final int lineNum, final int columnNum) {
        final Class<?> clazz = getLocalVariableType(lineNum, columnNum);

        if (null == clazz) {
            LOGGER.log(
                    Level.FINER,
                    "Cursor[lineNum{0}, columnNum={1}] has no local variable, try to find method return type(TODO....)",
                    new Object[]{lineNum, columnNum});
            // TODO: try method return type
        }

        return null == clazz ? null : clazz.getMethods();
    }

    /**
     * Loads a class from import declaration names for the specified class
     * simple name.
     * .
     * @param classSimpleName the specified class simple name
     * @return a class from imports
     */
    private Class<?> loadClassFromImports(final String classSimpleName) {
        Class<?> ret = null;
        final ClassLoader classLoader = NideJavaParser.class.getClassLoader();

        final List<String> importNames = getImportNames();
        try {
            for (final String importImage : importNames) {
                if (importImage.endsWith(classSimpleName)) {
                    ret = classLoader.loadClass(importImage);
                }
            }
        } catch (final ClassNotFoundException ex) {
            LOGGER.log(Level.SEVERE, ex.getMessage(), ex);
        }

        return ret;
    }

    /**
     * Loads a class by the specified canonical name.
     *
     * @param canonicalName the specified canonical name
     * @return a class corresponding to the specified canonical name if found,
     * returns {@code null} otherwise
     */
    private Class<?> loadClassByCanonicalName(final String canonicalName) {
        try {
            return ClassLoader.getSystemClassLoader().loadClass(canonicalName);
        } catch (final ClassNotFoundException e) {
            return null;
        }
    }

    /**
     * Loads a class under java.lang package for the specified simple name.
     *
     * @param simpleName the specified simple name
     * @return a class under java.lang if found, returns {@code null} otherwise
     */
    private Class<?> loadClassUnderJavaLangPackage(final String simpleName) {
        try {
            return ClassLoader.getSystemClassLoader().loadClass("java.lang."
                                                                + simpleName);
        } catch (final ClassNotFoundException e) {
            return null;
        }
    }
}
